﻿Imports System.Reflection

''' <summary>
''' Provides configuration of, and control over, processing for a command-driven console application.
''' </summary>
''' <remarks></remarks>
Public NotInheritable Class CommandInterpreter
    ''' <summary>
    ''' Occurs when a command line is being processed.
    ''' </summary>
    ''' <remarks></remarks>
    Public Shared Event ProcessCommandLine As EventHandler(Of ProcessCommandLineEventArgs)
    ''' <summary>
    ''' Occurs after a command line has been sucessfully processed.
    ''' </summary>
    ''' <remarks></remarks>
    Public Shared Event ProcessCommandLineComplete As EventHandler(Of ProcessCommandLineEventArgs)
    ''' <summary>
    ''' Occurs after a command line has finished processing with errors.
    ''' </summary>
    ''' <remarks></remarks>
    Public Shared Event ProcessCommandLineError As EventHandler(Of ProcessCommandLineEventArgs)
    ''' <summary>
    ''' Occurs when the command prompt text has changed.
    ''' </summary>
    ''' <remarks></remarks>
    Public Shared Event CommandPromptChanged As EventHandler(Of CommandPromptChangedEventArgs)

    ''' <summary>
    ''' Provides the shared instance of the CommandInterpreter.
    ''' </summary>
    ''' <remarks>This field is used primarly by the event chain to provide a sender object.</remarks>
    Public Shared ReadOnly Self As CommandInterpreter = New CommandInterpreter

    Protected Friend Shared _ExecutingLine As CommandLine

    ''' <summary>
    ''' Gets or sets a value determining whether or not command names are treated as case-sensitive.
    ''' </summary>
    ''' <value>Determines if command names are case-sensitive.</value>
    ''' <returns>True if command names are case-sensitive; otherwise false. Default value is false.</returns>
    ''' <remarks></remarks>
    Public Shared Property CaseSensitive As Boolean = False

    ''' <summary>
    ''' Gets or sets the class name used by the interpreter when logging command errors.
    ''' </summary>
    ''' <value>Determines the name to use when logging command processing errors.</value>
    ''' <returns>The name to use when logging command processing errors.</returns>
    ''' <remarks></remarks>
    Public Shared Property CommandErrorClassName As String = "Error"

    Private Shared _CommandIdentifierToken As String = String.Empty
    ''' <summary>
    ''' Gets or sets the string used to identify a command.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property CommandIndentifierToken As String
        Get
            Return _CommandIdentifierToken
        End Get
        Set(value As String)
            If value Is Nothing Then value = String.Empty
            If Not _CommandIdentifierToken = value Then
                _CommandIdentifierToken = value
            End If
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the class name used by the interpreter when logging command process completion.
    ''' </summary>
    ''' <value>Determines the name to use when logging command process completion.</value>
    ''' <returns>The name to use when logging command process completion.</returns>
    ''' <remarks></remarks>
    Public Shared Property CommandProcessedClassName As String = "Processed"

    ''' <summary>
    ''' Gets or sets the error number used by the interpreter when logging a command processing error.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property CommandProcessingErrorNumber As Integer = 500

    Private Shared _CommandPrompt As String = ":>"
    ''' <summary>
    ''' Gets or sets the command prompt string to display in the console.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property CommandPrompt As String
        Get
            Return _CommandPrompt
        End Get
        Set(value As String)
            If value Is Nothing Then value = String.Empty
            If Not _CommandPrompt = value Then
                Dim oldPrompt As String = _CommandPrompt
                _CommandPrompt = value
                OnPromptChanged(New CommandPromptChangedEventArgs(oldPrompt, value))
            End If
        End Set
    End Property

    Private Shared _CommandResults As New CommandResultCollection
    ''' <summary>
    ''' Contains the results of all commands executed since the last Clear command.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared ReadOnly Property CommandResults As CommandResultCollection
        Get
            Return _CommandResults
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets the code used by the interpreter when logging command completion.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property CommandSuccessfulCode As Integer = 0
    ''' <summary>
    ''' Gets or sets the default command prompt string.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property DefaultCommandPrompt As String = ":>"

    Private Shared _DefaultCommands As DefaultCommands
    ''' <summary>
    ''' Gets or sets the class instance containing the interpreter's default command set.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property DefaultCommands As DefaultCommands
        Get
            Return _DefaultCommands
        End Get
        Set(value As DefaultCommands)
            If value IsNot Nothing AndAlso value IsNot _DefaultCommands Then
                If _DefaultCommands IsNot Nothing Then _Registration.RemoveObjectRegistrations(_DefaultCommands)
                _DefaultCommands = value
                _Registration.RegisterObject(_DefaultCommands)
            End If
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the name used by the interpreter when logging.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property InterpreterSourceName As String = "Interpreter"

    Protected Friend Shared _IsRunning As Boolean
    Public Shared ReadOnly Property IsRunning As Boolean
        Get
            Return _IsRunning
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets the log level code for informational log entries.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property LogLevelInformation As Integer = 0

    ''' <summary>
    ''' Gets or sets the log level code for warning log entries.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property LogLevelWarning As Integer = 1

    ''' <summary>
    ''' Gets or sets the log level code for error log entries.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property LogLevelError As Integer = 2

    ''' <summary>
    ''' Gets or sets a value determining whether or not the interpreter stores the result of each processed command line.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property RecordResults As Boolean = True

    Protected Friend Shared _Registration As New CommandMethodCollection
    ''' <summary>
    ''' Gets the collection of command methods registered to the interpreter.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared ReadOnly Property Registration As CommandMethodCollection
        Get
            Return _Registration
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets the number of spaces to indent a command line result when writing the output.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property ResultIndentation As Integer = 3

    ''' <summary>
    ''' Gets or sets the text displayed for an unknown command error.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property UnrecognizedCommandText As String = "COMMAND NOT RECOGNIZED"

    ''' <summary>
    ''' Gets or sets the text displayed when the interperter is first run.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property WelcomeBanner As String = String.Format("\[c6]{0}\[n1]Version {1}\[n22]Type HELP for a list of available commands\[n]", My.Application.Info.ProductName, My.Application.Info.Version)

    ''' <summary>
    ''' Gets or sets the Regex pattern used to split a line of input into words.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property WordMatchPattern As String = "(\S*\s*)"

    Protected Sub New()
    End Sub

    ''' <summary>
    ''' Resets the command prompt to default.
    ''' </summary>
    ''' <remarks></remarks>
    Public Shared Sub ResetPrompt()
        CommandPrompt = DefaultCommandPrompt
    End Sub

    ''' <summary>
    ''' Starts the interpreter.
    ''' </summary>
    ''' <remarks></remarks>
    Public Shared Sub Run()
        If DefaultCommands Is Nothing Then
            DefaultCommands = New DefaultCommands
        End If
        For Each t As Type In Assembly.GetCallingAssembly.GetTypes
            If t.IsClass AndAlso t.IsSealed AndAlso t.GetConstructors(BindingFlags.Public).Length = 0 Then
                For Each method As MethodInfo In t.GetMethods(BindingFlags.Static Or BindingFlags.Public Or BindingFlags.NonPublic)
                    Dim attribs() As Attribute = method.GetCustomAttributes(GetType(CommandMethodAttribute), True)
                    If attribs.Length > 0 Then
                        Dim argAttribs() As Attribute = method.GetCustomAttributes(GetType(CommandMethodArgumentAttribute), True)
                        Registration.AddNew(attribs(0), method, method.Module, argAttribs)
                    End If
                Next
            End If
        Next
        _IsRunning = True
        ConsoleOutput.WriteFormatted(WelcomeBanner)
        Do
            If CommandPrompt.Length > 0 Then ConsoleOutput.Write(CommandPrompt)
            Dim consoleInput As String = Console.ReadLine
            Dim line As New CommandLine(consoleInput)
            line._IsProcessing = True
            Dim args As ProcessCommandLineEventArgs = OnProcessLine(line)
            line.Result.Stamp()
            If line.Result.IsError Then
                line.LogEvent(LogLevelError, InterpreterSourceName, CommandErrorClassName, CommandProcessingErrorNumber, line.Result.ErrorText)
                ConsoleOutput.WriteIndentedColored(ResultIndentation, ConsoleColor.Red, True, line.Result.ErrorText)
                OnProcessCommandLineError(args)
            Else
                line.LogEvent(LogLevelInformation, InterpreterSourceName, CommandProcessedClassName, CommandSuccessfulCode, line.CommandName)
                If line.Result.ResultText.Length > 0 Then
                    Dim indentPrefix As String = String.Empty
                    If ResultIndentation > 0 Then indentPrefix = ConsoleOutput.GetCursorRightFormatCode(ResultIndentation)
                    ConsoleOutput.WriteFormatted(indentPrefix & line.Result.ResultText)
                End If
                OnProcessCommandLineComplete(args)
            End If
            line._IsProcessing = False
            If RecordResults Then _CommandResults.Add(line.Result)
            ConsoleOutput.WriteLine(1)
            System.Threading.Thread.Sleep(0)
        Loop While _IsRunning
    End Sub

    Protected Shared Function OnProcessLine(line As CommandLine) As ProcessCommandLineEventArgs
        _ExecutingLine = line
        If _Registration.AlternateCommandNames.ContainsKey(line.CommandName.ToLower) Then
            line.CommandName = _Registration.AlternateCommandNames(line.CommandName.ToLower)
        End If
        Dim args As ProcessCommandLineEventArgs
        If line.CommandName.Length > 0 AndAlso _Registration.Contains(line.CommandName) Then
            args = New ProcessCommandLineEventArgs(line, True)
        Else
            args = New ProcessCommandLineEventArgs(line, False)
        End If
        RaiseEvent ProcessCommandLine(CommandInterpreter.Self, args)
        If args.HandledByCommandMethod AndAlso _Registration.Contains(line.CommandName) Then
            Dim command As RegisteredCommandMethod = _Registration(line.CommandName)
            command.MethodInfo.Invoke(command.Target, New Object() {line})
        ElseIf Not args.HandledByCustom AndAlso line.CommandName.Length > 0 Then
            line.SetResultMessage(UnrecognizedCommandText, True)
        End If
        _ExecutingLine = Nothing
        Return args
    End Function

    Protected Shared Sub OnProcessCommandLineComplete(e As ProcessCommandLineEventArgs)
        RaiseEvent ProcessCommandLineComplete(Self, e)
    End Sub

    Protected Shared Sub OnProcessCommandLineError(e As ProcessCommandLineEventArgs)
        RaiseEvent ProcessCommandLineError(Self, e)
    End Sub

    Protected Shared Sub OnPromptChanged(e As CommandPromptChangedEventArgs)
        RaiseEvent CommandPromptChanged(Self, e)
    End Sub
End Class
